//
//  PLPlatform.m
//  Plankton
//
//  Created by Marc Schlichte on 2008-07-12.
//  Copyright 2008-2009 Framework Labs. All rights reserved.
//

#import "PLPlatform.h"

#import "PLPattern.h"

@interface PLPattern (InternalMethods)

@property(nonatomic, retain) NSString *receiver;

- (id<PLMessageHandler>)messageHandler;
- (id<PLTimeoutHandler>)timeoutHandler;

- (NSTimeInterval)timeoutInterval;
- (void)setTimeoutTimer:(NSTimer *)timer;
- (void)invalidate;

@end

@interface PLPlatform (InternalMethods)

- (NSString *)createAidWithName:(NSString *)name;

- (void)postData:(NSData *)data;

- (BOOL)deliverAnyMessageForPattern:(PLPattern *)pattern;
- (BOOL)deliverMessage:(id)message forPattern:(PLPattern *)pattern;

- (void)removePatternFromPatterns:(PLPattern *)pattern;

- (id)internalizeFromData:(NSData *)data;
- (NSData *)externalizeToData:(id)message;

- (NSString *)aidForHost:(NSString *)host port:(unsigned short)port name:(NSString *)name;

- (void)noteProblem:(NSString *)problem inSubsystem:(NSString *)subsystem;

@end


@implementation PLPlatform

- (id)init
{
    if (self = [super init]) {
        patterns = [[NSMutableDictionary alloc] init];
        messages = [[NSMutableArray alloc] init];
        endpoint = [[PLEndpoint alloc] init];
        endpoint.delegate = self;
    }
    return self;
}

- (void)dealloc
{
    [endpoint release];
    [patterns release];
    [messages release];
    [super dealloc];
}

- (NSString *)makeAid
{
    NSString *aid = [self createAidWithName:[NSString stringWithFormat:@"%d", nextLocalAid++]];
	[patterns setObject:[NSMutableArray array] forKey:aid];
	return aid;
}
	
- (void)destroyAid:(NSString *)aid
{
	// cancel patterns of that aid which might wait for timeouts
	for (PLPattern *pattern in [patterns objectForKey:aid]) {
		[self cancelReceive:pattern];
	}
	
	// prevent new patterns to be registered for that aid by clearing that slot plus freeing memory
	[patterns removeObjectForKey:aid];
	
	// remove messages from message-queue which are sent to the aid
	for (id messageEnvelope in [[messages copy] autorelease]) {
		if ([[messageEnvelope objectForKey:@"receiver"] isEqual:aid]) {
			[messages removeObjectIdenticalTo:messageEnvelope];
		}
	}
}

+ (int)portOfAid:(NSString *)aid
{
    NSURL *url = [NSURL URLWithString:aid];
    return [[url port] intValue];
}

- (void)send:(id)message to:(NSString *)aid
{   
    // ignore sending nil messages or sending to nil aids
    if (!message || !aid) {
        return;
    }
    NSParameterAssert([aid isKindOfClass:[NSString class]]);
	
    // create message envelope
    NSDictionary *messageEnvelope = [NSDictionary dictionaryWithObjectsAndKeys:
									 message, @"content",
									 aid, @"receiver", nil];
    
    // convert message to data object
    NSData *data = [self externalizeToData:messageEnvelope];
    if (!data) {
        [NSException raise:NSInvalidArgumentException format:@"message to send is nil or is not in plist format: %@", messageEnvelope];
    }
    
    // convert aid to host and port
    NSURL *url = [NSURL URLWithString:aid];
    NSString *host = [url host];
    unsigned short port = [[url port] unsignedShortValue];
    if (!host || !port) {
        [NSException raise:NSInvalidArgumentException format:@"invalid host or port of target aid:%@", aid];
    }
    
    // if we send to same platform, post it directly, otherwise send through endpoint 
    if (port == endpoint.port && [host isEqual:endpoint.host]) {
        [self postData:data];
    }
    else {
        [endpoint sendData:data toHost:host port:port];
    }
}

- (void)multicast:(id)message to:(id<NSFastEnumeration>)aids
{
	id nullNull = [NSNull null];
    for (id aid in aids) {
		[self send:message to:aid == nullNull ? nil : aid];
    }
}

- (void)receive:(PLPattern *)pattern by:(NSString *)receiver
{    
	NSParameterAssert([receiver isKindOfClass:[NSString class]]);

    // store away the new pattern under the given receiver aid
	NSMutableArray *receiverPatterns = [patterns objectForKey:receiver];
	if (!receiverPatterns) {
		[NSException raise:NSInvalidArgumentException format:@"receiver:%@ is not an active actor id", receiver];
	}
    [receiverPatterns addObject:pattern];
	
	// assign receiver to pattern to be able to quickly look up the receiver again
	pattern.receiver = receiver;
    
    // check if this new pattern matches any of the pending messages
    BOOL deliveredAny = [self deliverAnyMessageForPattern:pattern];

    // start timeout timers if indicated
    if (!deliveredAny && [pattern timeoutHandler]) {
        NSTimeInterval interval = [pattern timeoutInterval];
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                          target:self 
                                                        selector:@selector(_onTimer:) 
                                                        userInfo:pattern 
                                                         repeats:NO];
        [pattern setTimeoutTimer:timer];
        
        // fire timer immediatly if interval is zero
        if (interval == 0.0) {
            [timer fire];
        }
    }
}

- (void)cancelReceive:(PLPattern *)pattern
{
	// stop any timeout timers of this pattern
    [pattern invalidate];
	
	// remove pattern from list of patterns handled by this platform
	[self removePatternFromPatterns:pattern];
}

- (BOOL)isReceiving:(PLPattern *)pattern
{
	return pattern.receiver != nil;
}

#if NS_BLOCKS_AVAILABLE

- (void)receive:(NSPredicate *)predicate by:(NSString *)aid 
	  withBlock:(void (^)(id message, PLPattern *))messageBlock
   timeoutBlock:(void (^)(PLPattern *))timeoutBlock after:(NSTimeInterval)timeoutInterval
{
	NSParameterAssert(messageBlock);
	
	PLPattern *pattern = [PLPattern patternWithPredicate:predicate];
	[pattern setMessageBlock:messageBlock];
	if (timeoutBlock) {
		[pattern setTimeoutBlock:timeoutBlock after:timeoutInterval];
	}
	
	[self receive:pattern by:aid];
}

#endif


#pragma mark internal methods

- (void)_onTimer:(NSTimer *)timer
{
    PLPattern *pattern = [timer userInfo];
    
    // remove pattern from list of patterns but still hold on to it
    [pattern retain];
	[self removePatternFromPatterns:pattern];
    
    // call the timeout
    [[pattern timeoutHandler] handleTimeoutForPattern:pattern];
    
    // give up upon pattern
    [pattern release];
}

- (NSNumber *)createTag
{
    return [NSNumber numberWithUnsignedLongLong:++nextTag];
}

/// endpoint delegate methods

- (void)endpoint:(PLEndpoint *)endpoint didReceiveData:(NSData *)data from:(NSData *)addr
{
    [self postData:data];
}

- (void)endpoint:(PLEndpoint *)endpoint didEncounterProblem:(NSString *)problem
{
    [self noteProblem:problem inSubsystem:@"endpoint"];
}

/// internal methods

- (NSString *)createAidWithName:(NSString *)name
{
    return [self aidForHost:[endpoint host] port:[endpoint port] name:name];
}

- (void)postData:(NSData *)data
{
	// convert data back to plist objects
    NSDictionary  *messageEnvelope = [self internalizeFromData:data];
	NSString *receiver = [messageEnvelope objectForKey:@"receiver"];
	
	// silently discard incoming messages to an unknown receiver
	NSArray *receiverPatterns = [patterns objectForKey:receiver];
	if (!receiverPatterns) {
		return;
	}
		
	// add additional checks here - like message integrity or blacklists etc.
    
	// try to deliver the message to any of the patterns for this receiver
    for (PLPattern *pattern in receiverPatterns) {
        if ([self deliverMessage:messageEnvelope forPattern:pattern]) {
            return;
        }
    }       
    
    // could not immediatly deliver message, so add to message queue
    [messages addObject:messageEnvelope];
}

- (BOOL)deliverAnyMessageForPattern:(PLPattern *)pattern
{
	NSString *receiver = pattern.receiver;
    for (id messageEnvelope in messages) {
		if ([[messageEnvelope objectForKey:@"receiver"] isEqual:receiver]) {
			if ([self deliverMessage:messageEnvelope forPattern:pattern]) {
				[messages removeObject:messageEnvelope];
				return YES;
			}
		}
    }
    return NO;
}

- (BOOL)deliverMessage:(id)messageEnvelope forPattern:(PLPattern *)pattern
{
	id message = [messageEnvelope objectForKey:@"content"];
    if ([pattern matches:message]) {
        
        // remove pattern from list of patterns but don't free it yet.
        [pattern retain];
        
        // stop any timers
        [pattern invalidate];
        [self removePatternFromPatterns:pattern];
		
        // let handler of matched-pattern handle the message
        id<PLMessageHandler> handler = [pattern messageHandler];
        if (handler) {
            [handler handleMessage:message forPattern:pattern];
        }
        
        // now free the pattern
        [pattern release];
        
        return YES;
    }
    else {
        return NO;
    }
}

- (void)removePatternFromPatterns:(PLPattern *)pattern
{
	[[patterns objectForKey:pattern.receiver] removeObjectIdenticalTo:pattern];
	pattern.receiver = nil;
}

- (id)internalizeFromData:(NSData *)data
{
    NSString *error;
    id res = [NSPropertyListSerialization propertyListFromData:data 
                                              mutabilityOption:NSPropertyListImmutable 
                                                        format:NULL 
                                              errorDescription:&error];
    if (error) {
        [self noteProblem:error inSubsystem:@"internalizeData"];
        [error release];
    }
    return res;
}

- (NSData *)externalizeToData:(id)msg
{
    NSString *error;
    NSData *res = [NSPropertyListSerialization dataFromPropertyList:msg 
                                                             format:NSPropertyListBinaryFormat_v1_0
                                                   errorDescription:&error];
    if (error) {
        [self noteProblem:error inSubsystem:@"externalizeData"];
        [error release];
    }
    return res;
}

- (NSString *)aidForHost:(NSString *)host port:(unsigned short)port name:(NSString *)name
{
    // actor-ids (aids) will be of form: plap://machine:port/name
    return [NSString stringWithFormat:@"plap://%@:%d/%@", host, port, name];
}

- (void)noteProblem:(NSString *)problem inSubsystem:(NSString *)subsystem
{
    NSLog(@"problem in '%@': %@", subsystem, problem);
}

@end
